#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# vim: expandtab tabstop=4 softtabstop=4 shiftwidth=4
#
# Author: Liang Guanchao <lianggc@gmail.com>
# 2010/09/04
#
# This script is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License V2 as published 
# by the Free Software Foundation.
# 
# Ref: http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
#
# This script is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY. Use it at your own risk. Don't forget to back up
# your data!
#
#
""" Command line client of Windows Live Skydrive.
"""
import os
import sys
import cmd
import codecs
import getpass
import getopt
import unicodedata
from threading import Thread
from time import sleep
from fnmatch import fnmatch

import skydriveapi

class ProgressBar(Thread):
    """A simple text progress bar control."""

    def __init__(self):
        self.running = True
        self.percent = -1
        self.msg = ""
        Thread.__init__(self)

    def run(self):
        i = 1
        while self.running and self.percent < 100:
            display = '\r'

            if self.msg != "": 
                display += self.msg + ' '

            if self.percent >= 0:
                display += str(self.percent) + '% '
                display += '-' * (40 * self.percent / 100)

            if i + 1 + len(display) > 80: 
                i = 1

            display += '-' * i
            display += '>'
            display += ' ' * (80 - len(display))
            sys.stderr.write(display)

            i += 1
            sleep(0.5)

        sys.stderr.write('\r' + ' ' * 79 + '\r')
        sys.stderr.flush()

    def stop(self):
        """Stop displaying progress bar.

        Note: there may be latency to stop. You'd better wait for the thread
        stops. See _stop_progress(t_bar).

        """
        self.running = False

    def set_percent(self, percent, msg = ""):
        """Call back method for owner of progress bar."""
        self.percent = int(percent)
        self.msg = msg
    
    def is_alive(self):
        return self.isAlive()


def _start_progress():
    """Display a progress bar"""
    t_bar = ProgressBar()
    t_bar.start()
    return t_bar 

def _stop_progress(t_bar):
    """Hide the progress bar"""
    if hasattr(t_bar, "is_alive"):
        alive = t_bar.is_alive
    else:
        alive = t_bar.isAlive

    if t_bar and alive():
        t_bar.stop()
        t_bar.join()

def _print_msg(t_bar, *args):
    """Hide the progress bar and print message"""

    assert t_bar is None or isinstance(t_bar, ProgressBar)

    if t_bar and t_bar.is_alive(): 
        _stop_progress(t_bar)

    print ' '.join(args)

def _unicode_str_width(text):
    """Calculate the exact width of unicode string."""

    width = 0
    for char in text:
        if isinstance(char, unicode):
            if unicodedata.east_asian_width(char) in ('F', 'W', 'A'):
                width += 2
            else:
                width += 1
        else:
            width += 1

    return width

def _split_arg(arg):
    """Split command line parameters.

    It works like what is done before shell parameters is put into sys.argv.

    """
    params = []

    word = ""
    prev = ""
    in_quote = ""
    for char in arg:
        if in_quote != "":
            if prev == '\\':
                word += char
            elif char == in_quote:
                in_quote = ""
                params.append(word)
                word = ""
            elif char != '\\':
                word += char
        else:
            if char in ('"', "'") and prev != '\\' and (prev == "" or 
                    prev.isspace()):
                in_quote = char
                if word.strip() != "": 
                    params.append(word)
                    word = ""
            elif char.isspace() and prev != '\\':
                if word.strip() != "": 
                    params.append(word)
                    word = ""
            elif char != '\\':
                word += char
        prev = char

    if word.strip() != "": 
        params.append(word)
    return params

class SkyDriveCmd(cmd.Cmd):
    def __init__(self, complete_key = 'Tab', stdin = None, stdout = None):
        cmd.Cmd.__init__(self, complete_key, stdin, stdout)

        self._account = None
        self._oldpwd = ""
        self._config_path = skydriveapi.get_config_path()

    def precmd(self, line):
        """Make sure the user is logged in when running specific commands."""
        required_login_cmds = ["ls", "cd", "pwd", "mkdir", "get", "mget",
                "mirror", "rename", "mkdir", "del", "put", "mput"]

        if line != "" and line.split()[0] in required_login_cmds:
            if self._account is None or not self._account.is_authorized():
                print "You haven't logged in."
                return ""

        if sys.stdin.encoding:
            line = line.decode(sys.stdin.encoding)

        return line
            
    def emptyline(self):
        """Disable repeating last non-empty command for empty line."""
        pass

    def do_login(self, arg):
        """Log in Skydrive account."""
        args = _split_arg(arg)

        user_name = ""
        passwd = ""

        if len(args) > 0:
            user_name = args[0]

        if len(args) > 1:
            passwd = args[1]

        while user_name == "": 
            user_name = raw_input("User: ")

        self._account = skydriveapi.SkyDriveAccount(user_name)
        cookie_file = self._account.get_cookie_path()

        if os.access(cookie_file, os.F_OK) and passwd == "":
            print "Cookie found. Try to verify..."
            t_bar = _start_progress()
            try:
                self._account.login_with_cookie(t_bar.set_percent)
            except skydriveapi.SkyDriveApiError, err:
                _print_msg(t_bar, "Failed to log in with cookie. Error: ", 
                        err.msg)
            else:
                _print_msg(t_bar, "OK. Logged in successfully. ")
                self.prompt = user_name + " > "
                return

        while passwd == "": 
            passwd = getpass.getpass("Password: ")

        t_bar = _start_progress()
        try:
            self._account.login_with_passwd(passwd, t_bar.set_percent)
        except skydriveapi.SkyDriveApiError, err: 
            _print_msg(t_bar, "Failed to log in. Error: ", err.msg)
            err.print_exception()
        else:
            _print_msg(t_bar, "OK. Logged in successfully.")
            self.prompt = user_name + " > "
        _stop_progress(t_bar)

    def help_login(self):
        """Print help message for command login."""
        print "Usage: login [email] [password]"

    def do_logout(self, arg):
        """Delete saved cookies."""
        self._account = None
        self.prompt = "Skydrive > "
        # FIXME: remove cookie file?


    def help_logout(self):
        """Print help message for command logout."""
        print "Usage: logout"
        print "log out current user."

    def do_ls(self, arg):
        if arg.strip() == "": 
            arg = "."
        args = _split_arg(arg)
        if len(args) > 1:
            self.help_ls()
            return

        t_bar = _start_progress()
        try:
            items = self._account.listdir(args[0])
        except skydriveapi.SkyDriveApiError, err:
            _print_msg(t_bar, err.msg)
            return
        _stop_progress(t_bar)

        if items is None or len(items) == 0: 
            # Nothing to output
            return

        max_name = max(_unicode_str_width(item.name) for item in items)
        max_owner = max(_unicode_str_width(item.owner) for item in items)
        #max_mtime = max(_unicode_str_width(item.mtime) for item in items)

        for item in items:
            if skydriveapi.isdir(item): 
                item_type = "<Dir>"
            else: 
                item_type = "<File>"

            # Note: the following code fixed the issue that east asian chars 
            # have a different width with ascii chars.
            # Maybe we can implement a more generic printf func.

            width = _unicode_str_width(item.name)
            line = item.name + ' ' * (max_name + 8 - width)

            width = _unicode_str_width(item_type)
            line = line + item_type + ' ' * (12 - width)

            width = _unicode_str_width(item.owner)
            line = line + item.owner + ' ' * (max_owner + 6 - width)

            width = _unicode_str_width(item.mtime)
            line = line + item.mtime

            print line

    def complete_ls(self, text, line, begin_idx, end_idx):
        return self._complete_remote(text, line, begin_idx, end_idx, 'dir')

    def help_ls(self):
        print """Usage: ls [directory_path]
list contents of directory_path. The syntax of directory path is the same as
cd (see: help cd)
"""

    def do_cd(self, arg):
        """Change current working directory to arg."""
        if arg.strip() == "-": 
            arg = self._oldpwd
            print arg

        if arg.strip() == "": 
            arg = "/"
            print arg

        t_bar = _start_progress()
        try:
            self._oldpwd = self._account.getcwd()
            self._account.chdir(arg)
        except skydriveapi.SkyDriveApiError, err:
            _print_msg(t_bar, str(err))

        _stop_progress(t_bar)

    def complete_cd(self, text, line, begin_idx, end_idx):
        return self._complete_remote(text, line, begin_idx, end_idx, 'dir')

    def help_cd(self):
        """Print help message for command cd."""
        print """Usage: cd <directory>
Change current working directory to <directory>. Examples:

cd -  # to previous directory
cd .. # to parent directory
cd .  # current directory

"." and ".." can be used repeatly, like "../../documents"
"""

    def do_pwd(self, arg):
        """Print current working directory (absolute path)."""
        try:
            print self._account.getcwd()
        except skydriveapi.SkyDriveApiError, err:
            print err.msg

    def help_pwd(self):
        """Print help message for command pwd."""
        print "Usage: pwd"
        print "print current working directory."

    def do_lcd(self, arg):
        """Change local working directory."""
        args = _split_arg(arg)
        if len(args) > 1:
            self.help_lcd()
            return

        try:
            os.chdir(args[0])
        except OSError, err:
            print err

    def complete_lcd(self, text, line, begidx, endidx):
        return self._complete_local(text, line, begidx, endidx, "dir")

    def help_lcd(self):
        print """Usage: lcd <local_dir>

Change local working directory.
"""

    def do_mkdir(self, arg):
        args = _split_arg(arg)
        if len(args) != 1:
            print self.help_mkdir()
            return

        t_bar = _start_progress()
        try:
            self._account.mkdir(args[0])
        except skydriveapi.SkyDriveApiError, err:
            _print_msg(t_bar, err.msg)
        else:
            _print_msg(t_bar, 'directory "%s" made' % arg)

    def help_mkdir(self):
        print "Usage: mkdir <directory>"

    def do_rename(self, arg):
        args = _split_arg(arg)
        if len(args) != 2:
            self.help_rename()
            return

        if args[0].find('/') >= 0:
            print "Only renaming in current working directory is supported."
            return

        if args[1].find('/') >= 0:
            print "rename doesn't support moving original file."
            return

        t_bar = _start_progress()
        try:
            self._account.rename(args[0], args[1])
        except skydriveapi.SkyDriveApiError, err:
            _print_msg(t_bar, err.msg)
        _stop_progress(t_bar)

    def complete_rename(self, text, line, begin_idx, end_idx):
        return self._complete_remote(text, line, begin_idx, end_idx, 'both')

    def help_rename(self):
        print "Usage: rename <file_or_dir> <new_name>"

    def do_rm(self, arg):
        args = _split_arg(arg)
        if len(args) > 1:
            print "Please delete one by one"
            return

        t_bar = _start_progress()
        try:
            self._account.delete(args[0])
        except skydriveapi.SkyDriveApiError, err:
            _print_msg(t_bar, err.msg)
        _stop_progress(t_bar)

    def complete_rm(self, text, line, begin_idx, end_idx):
        return self._complete_remote(text, line, begin_idx, end_idx, 'both')

    def help_rm(self):
        print "Usage: rm <file_or_dir>"

    def do_del(self, arg):
        self.do_rm(arg)

    def complete_del(self, text, line, begin_idx, end_idx):
        return self._complete_remote(text, line, begin_idx, end_idx, 'both')

    def help_del(self):
        print "Usage: del <file_or_dir>"

    def _movecopy(self, arg, copy = False):
        args = _split_arg(arg)
        if len(args) != 2:
            if copy:
                self.help_copy()
            else:
                self.help_move()
            return

        t_bar = _start_progress()
        try:
            if copy:
                self._account.copy(args[0], args[1])
            else:
                self._account.move(args[0], args[1])
        except skydriveapi.SkyDriveApiError, err:
            _print_msg(t_bar, err.msg)
        _stop_progress(t_bar)

    def do_move(self, arg):
        self._movecopy(arg, False)

    def complete_move(self, text, line, begin_idx, end_idx):
        args = _split_arg(line)
        item_type = "both"
        if len(args) == 3:
            item_type = "dir"

        return self._complete_remote(text, line, begin_idx, end_idx, item_type)

    def help_move(self):
        pass

    def do_mv(self, arg):
        self.do_move(arg)

    def complete_mv(self, text, line, begin_idx, end_idx):
        return self.complete_move(text, line, begin_idx, end_idx)

    def help_mv(self):
        print "Alias of move"

    def do_copy(self, arg):
        self._movecopy(arg, True)

    def complete_copy(self, text, line, begin_idx, end_idx):
        args = _split_arg(line)
        item_type = "file"
        if len(args) == 3:
            item_type = "dir"
        return self._complete_remote(text, line, begin_idx, end_idx, item_type)

    def help_copy(self):
        print "Usage: copy <file> <directory>"

    def do_cp(self, arg):
        self.do_copy(arg)

    def complete_cp(self, text, line, begin_idx, end_idx):
        return self._complete_remote(text, line, begin_idx, end_idx, 'file')

    def help_cp(self):
        print "Usage: cp <file> <directory>"
        print "cp is an alias of copy."

    def _get(self, arg, down_zip = False):
        def print_help():
            if down_zip:
                self.help_getzip()
            else:
                self.help_get()

        try:
            opts, args = getopt.getopt(_split_arg(arg), "ho:")
        except getopt.GetoptError, err:
            print str(err)
            print_help()
            return 

        path = ""
        out_file_name = ""

        for param, value in opts:
            if param in ("-h", "--help"):
                print_help()
                return
            elif param in ("-param", "--output"):
                out_file_name = value
            else:
                print "Unknown option: %s" % param
                return

        if len(args) > 0: 
            path = args[0]
        else:
            print_help()
            return
        
        t_bar = None

        try:
            item = self._account.find_item(path)
            if skydriveapi.isdir(item) and not down_zip:
                _print_msg(t_bar, "Can't get directory. Use getzip instead.")
                return

            if out_file_name == "": 
                out_file_name = item.name
                if skydriveapi.isdir(item): 
                    out_file_name += ".zip"

            print "downloading %s ..." % out_file_name
            t_bar = _start_progress()
            out_file = file(out_file_name, "wb")
            if down_zip:
                self._account.download_zip(path, out_file, t_bar.set_percent)
            else:
                self._account.download(path, out_file, t_bar.set_percent)
            _print_msg(t_bar, "done.")
        except skydriveapi.SkyDriveApiError, err:
            _print_msg(t_bar, err.msg)
            return
        else:
            if out_file in dir(): 
                out_file.close()

        _stop_progress(t_bar)

    def do_get(self, arg):
        self._get(arg, False)

    def complete_get(self, text, line, begin_idx, end_idx):
        return self._complete_remote(text, line, begin_idx, end_idx, 'file')

    def help_get(self):
        print """Usage: get [-o output_file_name] path

download remote file to local disk. If "-o out_file_name" is not provided, file
will be saved to local working directory with it's remote file name.  Otherwise
out_file_name will be used.
"""

    def do_getzip(self, arg):
        self._get(arg, True)

    def complete_getzip(self, text, line, begin_idx, end_idx):
        return self._complete_remote(text, line, begin_idx, end_idx, 'dir')

    def help_getzip(self):
        print """Usage: getzip [-o output_file_name] dir

download remote directory as zip file. If "-o out_file_name" is not provided, 
file will be saved to local working directory with file name 
"directory name + .zip".  Otherwise out_file_name will be used.
"""
    def do_mget(self, arg):
        args = _split_arg(arg)
        if len(args) < 1:
            self.help_mget()
            return

        try:
            items = self._account.listdir(".")
        except skydriveapi.SkyDriveApiError, err:
            print err.msg
            return

        match_list = []
        for item in items:
            for path in args:
                if skydriveapi.isfile(item) and fnmatch(item.name, path):
                    match_list.append(item.name)
                    break

        count = 0
        for path in match_list:
            count += 1
            print 'downloading "%s" [%d/%d]...' % (path, count, len(match_list))
            t_bar = _start_progress()
            try:
                item = self._account.find_item(path)
                out_file = file(item.name, "wb")
                self._account.download(path, out_file, t_bar.set_percent)
                out_file.close()
            except skydriveapi.SkyDriveApiError, err:
                _print_msg(t_bar, err.msg)
            else:
                _print_msg(t_bar, "done.")

    def complete_mget(self, text, line, begin_idx, end_idx):
        return self._complete_remote(text, line, begin_idx, end_idx, 'file')

    def help_mget(self):
        print """Usage: mget <pattern1> [pattern2] ... [patternN]

Download all files that match one of patterns [1-N] in current working 
directory. 

Pattern can contain the following wildcards:

*	matches everything
?	matches any single character
[seq]	matches any character in seq
[!seq]	matches any character not in seq

For example: mget *.pdf *.doc a??b.xls
"""

    def do_put(self, arg):
        args = _split_arg(arg)
        if len(args) > 1:
            print "You can upload only 1 file per time. Try mput instead."
            return

        path = args[0]
        size = os.path.getsize(path) #FIXME

        if size > 1024 * 1024 * 50:
            print '%s is bigger than 50M. Not allowed by skydrive.' % path
            return
        
        t_bar = _start_progress()
        try:
            self._account.upload(".", [path])
        except skydriveapi.SkyDriveApiError, err:
            _print_msg(t_bar, err.msg)

        _stop_progress(t_bar)

    def complete_put(self, text, line, begidx, endidx):
        return self._complete_local(text, line, begidx, endidx, "file")

    def help_put(self):
        print """Usage: put <local_file>

Upload a local file to remote working directory.
"""

    def do_mput(self, arg):
        args = _split_arg(arg)

        if len(args) < 1:
            self.help_mput()
            return

        to_upload_files = []
        for path in os.listdir('.'):
            for pattern in args:
                if fnmatch(path, pattern) and os.path.isfile(path):
                    to_upload_files.append(path)

        total_size = 0
        try:
            for path in to_upload_files:
                size = os.path.getsize(path)
                total_size += size
                if size >= 1024 * 1024 * 50:
                    print "%s >= 50M. Not allowed by SkyDrive." % path
                    return
        except OSError, err:
            print err
            return

        uploaded_size = 0
        total_files = len(to_upload_files)
        t_bar = _start_progress()
        try:
            i = 0
            while i * 5 < total_files:
                begin = i * 5
                if total_files - begin >= 5:
                    end = begin + 5
                else:
                    end = total_files

                self._account.upload(".", to_upload_files[begin:end])
                uploaded_size += sum([os.path.getsize(f) for f in to_upload_files[begin:end]])
                t_bar.set_percent(uploaded_size * 100 / total_size, "[%d/%d]" % (end, total_files))

                i += 1
        except skydriveapi.SkyDriveApiError, err:
            _print_msg(t_bar, err.msg)
        except OSError, err:
            _print_msg(t_bar, err)

        _stop_progress(t_bar)


    def complete_mput(self, text, line, begidx, endidx):
        return self._complete_local(text, line, begidx, endidx, "file")

    def help_mput(self):
        print """Usage: mput <pattern1> [pattern2] ... [patternN]

Upload multiple local files to remote working directory. This command will look
up files in local working directory that match one of the patterns.

Pattern can contain the following wildcards:

*	matches everything
?	matches any single character
[seq]	matches any character in seq
[!seq]	matches any character not in seq

For example: mput *.pdf *.doc a??b.xls
"""

    def do_mirror(self, arg):
        # TODO: the main reason I haven't done this is because it's not easy to
        # parse human readable time format without 3rd libs, for example, 
        # "n minutes ago" or "just now". Therefore I can't cache the structure 
        # of directories in local files as there is no way to check if it's 
        # outdate.
        print "Not implemented yet."

    def help_mirror(self):
        print "Not implemented yet."

    def do_shell(self, arg):
        os.system(arg)

    def help_shell(self):
        print "Usage: shell <os command>"
        print "       !<os command>"
        print "Execute shell commands"

    def do_exit(self, arg):
        return True

    def help_exit(self):
        print "Usage: exit"
        print "exit skydrive command line client"

    def do_quit(self, arg):
        return True

    def help_quit(self):
        print "Usage: quit"
        print "exit skydrive command line client"

    def do_EOF(self, arg):
        return True

    def do_about(self, arg):
        self.help_about()

    def help_about(self):
        print ("""
---------------------Python Skydrive Command Line Client-----------------------
-                                                                             -
-        License: GPL v2                                                      -
-        Site: http://code.google.com/p/pyskydrive                            -
-        Email: lianggc at gmail.com (please replace at with @)               -
-                                                                             -
-------------------------------------------------------------------------------
        """)

    def do_intro(self, arg):
        self.help_intro()

    def help_intro(self):
        print("""
------------------Python Skydrive Command Line Client--------------------------
        login [-p password] [email]
        logout
        cd
        pwd
        rename
        mkdir
        put
        get
        del
        rm
        """)
        pass

    def _complete_remote(self, text, line, begin_idx, end_idx, mtype = "both"):
        args = _split_arg(line)

        res = []
        text = text.lower()
        mtype = mtype.lower()

        parent_path = args[-1]
        if parent_path.startswith('"') or parent_path.startswith("'"):
            parent_path = parent_path[1:]

        if parent_path.rfind('/') > 0:
            parent_path = parent_path[0:parent_path.rfind('/')]
        elif parent_path.rfind('/') == 0:
            parent_path = '/'
        else:
            parent_path = '.'

        def match_type(item):
            if mtype == "both":
                return True
            elif mtype == "file" and skydriveapi.isfile(item):
                return True
            elif mtype == "dir" and skydriveapi.isdir(item):
                return True

            return False

        try:
            for item in self._account.listdir(parent_path):
                name = item.name.lower()
                if name.startswith(text) and match_type(item):
                    res.append(item.name)
        except skydriveapi.SkyDriveApiError, err:
            pass

        return res

    def _complete_local(self, text, line, begin_idx, end_idx, mtype = "both"):
        args = _split_arg(line)

        res = []
        text = text.lower()
        mtype = mtype.lower()

        parent_path = args[-1]
        if parent_path.startswith('"') or parent_path.startswith("'"):
            parent_path = parent_path[1:]

        parent_path = os.path.dirname(parent_path)
        if parent_path == "": parent_path = "."
       
        for name in os.listdir(parent_path):
            if not name.startswith(text):
                continue

            full_path = os.path.join(parent_path, name)

            if mtype == "both":
                res.append(name)
            elif mtype == "file" and os.path.isfile(full_path):
                res.append(name)
            elif mtype == "dir" and os.path.isdir(full_path):
                res.append(name)

        return res


def print_usage():
    print "Usage: %s [-f input_file_name] [-l output_file_name]" % sys.argv[0]

def main(args):
    try:
        opts, args = getopt.getopt(args, "hf:l:")
    except getopt.GetoptError, err:
        print err
        print_usage()
        return
    
    in_file_name = ""
    out_file_name = ""
    for param, value in opts:
        if param in ("-h", "--help"):
            print_usage()
            return
        elif param in ("-f", "--script"):
            in_file_name = value
        elif param in ("-l", "--log-file"):
            out_file_name = value
    
    try:
        if in_file_name != "":
            in_file = codecs.open(in_file_name, "r", "utf-8")
            sys.stdin = in_file
        else:
            in_file = sys.stdin

        if out_file_name != "":
            out_file = codecs.open(out_file_name, "w", "utf-8")
            sys.stdout = out_file
        else:
            out_file = sys.stdout
    except IOError, err:
        print err
        return

    console = SkyDriveCmd('Tab', in_file, out_file)
    console.prompt = "Skydrive > "
    if in_file_name != "":
        console.use_rawinput = 0

    console.cmdloop("Welcome to skydrive. Type intro and enter for "
                    "introduction.")

if __name__ == "__main__":
    main(sys.argv[1:])

